package checker

import (
	"mi/compiler/ast"
	"mi/compiler/binder"
	"mi/compiler/token"
)

// Language service support
func (c *checker) getSymbolsInScope(node ast.Node, meaning binder.SymbolFlags) []*binder.Symbol {
	var symbols = make(map[string]*binder.Symbol)
	var memberFlags ast.ModifierFlags

	var copySymbol = func(symbol *binder.Symbol, meaning binder.SymbolFlags) {
		if symbol.Flags&meaning != 0 {
			var id = symbol.Name
			if !c.isReservedMemberName(id) && symbols[id] == nil {
				symbols[id] = symbol
			}
		}
	}

	var copySymbols = func(source *binder.SymbolTable, meaning binder.SymbolFlags) {
		if meaning != 0 {
			for _, symbol := range source.GetSymbols() {
				copySymbol(symbol, meaning)
			}
		}
	}

	var location = node
	var stopFindInClassLikeOrInterfaceLike = false
	for !isNull(location) {
		if locals := binder.GetLocalsOfNode(location); locals != nil {
			copySymbols(locals, meaning)
		}

		switch location.(type) {
		case *ast.ClassDeclaration,
			*ast.EnumDeclaration,
			*ast.InterfaceDeclaration,
			*ast.AnnotationDeclaration,
			*ast.BodyDeclaration:
			if stopFindInClassLikeOrInterfaceLike {
				break
			}

			var symbol = c.getSymbolOfNode(location)
			var staticType  = c.getTypeOfSymbol(symbol)
			for _, member := range c.getMembersOfType(staticType, meaning) {
				copySymbol(member, meaning)
			}

			if memberFlags&ast.MFStatic == 0 {
				var declarationType = c.getDeclaredTypeOfSymbol(symbol)
				for _, member := range c.getMembersOfType(declarationType, meaning) {
					copySymbol(member, meaning)
				}
			}

			if !(ast.IsBodyDeclaration(location) || ast.IsClassLike(location) && ast.GetModifierFlags(location)&ast.MFStatic == 0){
				stopFindInClassLikeOrInterfaceLike = true
			}
		}

		memberFlags = ast.GetModifierFlags(location)
		location = location.Parent()
	}

	// Append import symbols
	for _, symbol := range c.getResolvedSourceFileImportSymbols(node) {
		if symbol.Flags&binder.Package != 0 {
			copySymbols(symbol.Exports, meaning&binder.Type)
		} else {
			copySymbol(symbol, meaning&binder.Type)
		}
	}

	// Append default import symbols
	for _, symbol := range c.defaultImportPackageSymbols {
		if symbol.Flags&binder.Package != 0 {
			copySymbols(symbol.Exports, meaning&binder.Type)
		} else {
			copySymbol(symbol, meaning&binder.Type)
		}
	}

	var symbolTableToSymbolArray = func(symbols map[string]*binder.Symbol) []*binder.Symbol {
		var result []*binder.Symbol
		for _, symbol := range symbols {
			result = append(result, symbol)
		}
		return result
	}
	return symbolTableToSymbolArray(symbols)
}

// True if the given identifier is the name of a type declaration ast (class, interface, enum, type parameter, etc)
func (c *checker) isTypeDeclarationName(name ast.Node) bool {
	return ast.IsIdentifier(name) &&
		c.isTypeDeclaration(name.Parent()) &&
		ast.GetDeclarationName(name.Parent().(ast.Declaration)) == name
}

func (c *checker) isTypeDeclaration(node ast.Node) bool {
	switch node.(type) {
	case *ast.ClassDeclaration,
		*ast.EnumDeclaration,
		*ast.InterfaceDeclaration,
		*ast.AnnotationDeclaration:
		return true
	}

	return false
}

// True if the given identifier is part of a type reference
func (c *checker) isTypeReferenceIdentifier(entityName ast.Node) bool {
	var node = entityName
	for node.Parent() != nil && ast.IsQualifiedName(node.Parent()) {
		node = node.Parent()
	}
	return node.Parent() != nil && (ast.IsTypeReference(node.Parent()) || ast.IsClassExpression(node.Parent()))
}

func (c *checker) isParameterTagIdentifier(node ast.Node) bool {
	switch n := node.Parent().(type) {
	case *ast.ParameterTag:
		return n.Name == node
	}
	return false
}

func (c *checker) forEachEnclosingTypeDeclaration(node ast.Node, callback func(n ast.Node, inStatic bool) bool) bool {
	var result = false
	var inStaticContext = false

	var current = node
	loop: for !isNull(current) {
		switch n := current.(type) {
		case *ast.MethodDeclaration:
			if c.getDeclarationModifierFlagsFromDeclaration(n)&ast.MFStatic != 0 {
				inStaticContext = true
			}

		case *ast.ClassDeclaration,
			*ast.EnumDeclaration,
			*ast.BodyDeclaration,
			*ast.InterfaceDeclaration,
			*ast.AnnotationDeclaration:

			if result = callback(n, inStaticContext); result {
				break loop
			}
			if !(ast.IsBodyDeclaration(current) || ast.IsClassLike(current) && ast.GetModifierFlags(current)&ast.MFStatic == 0){
				inStaticContext = true
			}

		}

		current = current.Parent()
	}

	return result
}

func (c *checker) isNodeWithinTypeDeclaration(node ast.Node, classDeclaration ast.Declaration) bool {
	return c.forEachEnclosingTypeDeclaration(node, func(n ast.Node, _ bool) bool {
		return n == classDeclaration
	})
}

func (c *checker) isNodeWithinTypeDeclarationAndIsInStaticContext(node ast.Node, classDeclaration ast.Declaration) (within bool, inStaticContext bool) {
	within = c.forEachEnclosingTypeDeclaration(node, func(n ast.Node, in bool) bool {
		if n == classDeclaration {
			inStaticContext = in
		}

		return false
	})

	return within, inStaticContext
}



func (c *checker) getSymbolOfEntityName(entityName ast.Node) *binder.Symbol {
	if ast.IsDeclarationName(entityName) {
		return c.getSymbolOfNode(entityName.Parent())
	}

	if ast.IsRightSideOfQualifiedNameOrSelectorExpression(entityName) {
		entityName = entityName.Parent()
	}

	if ast.IsPartOfExpression(entityName) {
		if ast.NodeIsMissing(entityName) {
			return nil
		}

		switch n := entityName.(type) {
		case *ast.Identifier:
			var symbol = c.getNodeLinks(entityName).resolvedSymbol
			if symbol == nil {
				c.checkIdentifier(n)
			}
			return c.getNodeLinks(entityName).resolvedSymbol
		case *ast.SelectorExpression:
			var symbol = c.getNodeLinks(entityName).resolvedSymbol
			if symbol == nil {
				c.checkSelectorExpression(n)
			}
			return c.getNodeLinks(entityName).resolvedSymbol
		case *ast.InstanceAccessExpression:
			var symbol = c.getNodeLinks(entityName).resolvedSymbol
			if symbol == nil {
				c.checkInstanceAccessExpression(n)
			}
			return c.getNodeLinks(entityName).resolvedSymbol
		}
	} else if c.isTypeReferenceIdentifier(entityName) {
		var meaning binder.SymbolFlags
		if ast.IsTypeReference(entityName.Parent()) ||
			ast.IsClassExpression(entityName.Parent()) {
			meaning = binder.Type
		} else {
			meaning = binder.Namespace
		}

		// Include Import in the meaning, this ensures that we do not follow aliases to where they point and instead
		// return the alias symbol.
		return c.resolveEntityName(entityName, entityName, meaning, entityName)
	} else if c.isParameterTagIdentifier(entityName) {
		return c.resolveSymbolFromParameterTagIdentifier(entityName.(*ast.Identifier))
	}

	// Do we want to return undefined here?
	return nil
}

func (c *checker) getSymbolAtLocation(node ast.Node) *binder.Symbol{
	if ast.IsDeclarationName(node) {
		// This is a declaration, call getSymbolOfNode
		return binder.GetSymbolOfNode(node.Parent())
	}

	switch n := node.(type) {
	case *ast.Identifier,
		*ast.SelectorExpression,
		*ast.QualifiedName:
		return c.getSymbolOfEntityName(node)
	case *ast.ThisExpression,
		*ast.SuperExpression:
		return c.getTypeOfExpression(node.(ast.Expression)).Symbol()
	case *ast.TokenNode:
		switch n.Token {
		case token.ThisKeyword:
			var expr = node.Parent().(ast.Expression)
			return c.getTypeOfExpression(expr).Symbol()
		case token.SuperKeyword:
			// May be super expression or bounds type
			var parent = node.Parent()
			switch pn := parent.(type) {
			case *ast.BoundsType:
				return c.getTypeFromTypeNode(pn).Symbol()
			case *ast.SuperExpression:
				return c.getTypeOfExpression(pn).Symbol()
			}
		case token.ConstructorKeyword:
			// constructor keyword for an overload, should take us to the definition if it exist
			var declaration = node.Parent()
			if !isNull(declaration) && ast.IsConstructorDeclaration(declaration) {
				return c.getSymbolOfNode(declaration.Parent())
			}
		}
	}

	return nil
}

func (c *checker) isUnknownSymbol(symbol *binder.Symbol) bool {
	return symbol == c.unknownSymbol
}

func (c *checker) getSymbolInfo(node ast.Node) *binder.Symbol {
	if ast.IsDeclarationName(node) {
		// This is a declaration, call getSymbolOfNode
		return c.getSymbolOfNode(node.Parent())
	}

	switch node.(type) {
	case *ast.Identifier,
		*ast.SelectorExpression,
		*ast.QualifiedName:
		return c.getSymbolOfEntityName(node)
	case *ast.ThisExpression,
		*ast.SuperExpression:
		var tpe = c.checkExpression(node.(ast.Expression), nil)
		return tpe.Symbol()
	case *ast.TokenNode:
		var n = node.(*ast.TokenNode)
		if n.Token == token.ConstructorKeyword {
			var constructorDeclaration = node.Parent()
			if constructorDeclaration != nil && ast.IsConstructorDeclaration(constructorDeclaration) {
				 return binder.GetSymbolOfNode(constructorDeclaration.Parent())
			}
		}
	}

	return nil
}

func (c *checker) getTypeOfNode(node ast.Node) Type {
	if ast.IsExpression(node) {
		return c.getTypeOfExpression(node.(ast.Expression))
	}
	if ast.IsTypeNode(node) {
		return c.getTypeFromNode(node)
	}

	if c.isTypeDeclaration(node) {
		// In this case, we call getSymbolOfNode instead of GetSymbolInfo because it is a declaration
		return c.getDeclaredTypeOfSymbol(binder.GetSymbolOfNode(node))
	}

	if c.isTypeDeclarationName(node) {
		var symbol = c.getSymbolInfo(node)
		return c.getDeclaredTypeOfSymbol(symbol)
	}

	return c.unknownType
}

func (c *checker) getTypeOfExpression(expr ast.Expression) Type {
	if ast.IsRightSideOfQualifiedNameOrSelectorExpression(expr) {
		expr = expr.Parent().(ast.Expression)
	}
	return c.checkExpression(expr, nil)
}

func (c *checker) getAugmentedPropertiesOfApparentType(tpe Type) []*binder.Symbol {
	var apparentType = c.getApparentType(tpe)
	var members = c.getMembersOfType(apparentType)
	return members
}

func (c *checker) getRootSymbol(symbol *binder.Symbol) *binder.Symbol {
	if symbol.Flags&binder.Transient != 0 {
		return c.getSymbolLinks(symbol).target
	} else {
		return symbol
	}
}

func (c *checker) getTypeOfSymbolAtLocation(symbol *binder.Symbol, location ast.Node) Type {
	// If we have an identifier or a property access at the given location, if the location is
	// an dotted name expression, and if the location is not an assignment target, obtain the type
	// of the expression (which will reflect control flow analysis). If the expression indeed
	// resolved to the given symbol, return the narrowed type.
	if ast.IsIdentifier(location) {
		if ast.IsRightSideOfQualifiedNameOrSelectorExpression(location) {
			location = location.Parent()
		}
		if ast.IsPartOfExpression(location) && !ast.IsAssignmentTarget(location) {
			var tpe = c.getTypeOfExpression(location.(ast.Expression))
			if c.getNodeLinks(location).resolvedSymbol == symbol {
				return tpe
			}
		}
	}
	// The location isn't a reference to the given symbol, meaning we're being asked
	// a hypothetical question of what type the symbol would have if there was a reference
	// to it at the given location. Since we have no control flow information for the
	// hypothetical reference (control flow information is created and attached by the
	// binder), we simply return the declared type of the symbol.
	return c.getTypeOfSymbol(symbol)
}
